package com.wb.spring.boot.autoconfigure.client;

import com.wb.spring.boot.autoconfigure.annotation.EnableWbClients;
import com.wb.spring.boot.autoconfigure.annotation.WbClient;
import com.wb.spring.boot.autoconfigure.proxy.ByteBuddyProxyFactory;
import com.wb.spring.boot.autoconfigure.proxy.WbClientProxy;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.config.*;
import org.springframework.beans.factory.support.*;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;

import static java.lang.Class.forName;

/**
 * @author deeprado
 * @create 2023-02-28 16:55
 */
public class WbClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    private ResourceLoader resourceLoader;
    private Environment environment;

    static void validateFallback(final Class clazz) {
        Assert.isTrue(!clazz.isInterface(),
                "Fallback class must implement the interface annotated by @WbClient");
    }

    static void validateFallbackFactory(final Class clazz) {
        Assert.isTrue(!clazz.isInterface(), "Fallback factory must produce instances "
                + "of fallback classes that implement the interface annotated by @WbClient");
    }


    public static String getName(String name) {
        if (!StringUtils.hasText(name)) {
            return "";
        }

        String host = null;
        try {
            String url;
            if (!name.startsWith("http://") && !name.startsWith("https://")) {
                url = "http://" + name;
            } else {
                url = name;
            }
            host = new URI(url).getHost();

        } catch (URISyntaxException e) {
        }
        Assert.state(host != null, "Service id not legal hostname (" + name + ")");
        return name;
    }

    static String getUrl(String url) {
        if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) {
            if (!url.contains("://")) {
                url = "http://" + url;
            }
            try {
                new URL(url);
            } catch (MalformedURLException e) {
                throw new IllegalArgumentException(url + " is malformed", e);
            }
        }
        return url;
    }

    static String getPath(String path) {
        if (StringUtils.hasText(path)) {
            path = path.trim();
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (path.endsWith("/")) {
                path = path.substring(0, path.length() - 1);
            }
        }
        return path;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
        Map<String, Object> annotationAttributes = getAnnotationAttributes(metadata, EnableWbClients.class);
        if (annotationAttributes == null) {
//            logger.warn(String.format("class %s 's annotationAttributesMap is null, metadata: %s",
//                    WbClient.class.getName(), metadata.getClass().getName()));
            return;
        }
        this.registerDefaultConfiguration(metadata, registry);

        this.registerWbClients(metadata, registry);
    }

    /**
     * 注册默认配置。默认配置均以"default." 开头
     */
    private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        Map<String, Object> annotationAttributes = getAnnotationAttributes(metadata, WbClient.class);
        if (annotationAttributes == null) {
//            log.warn(String.format("class %s 's annotationAttributesMap is null, metadata: %s",
//                    WbClient.class.getName(), metadata.getClass().getName()));
            return;
        }
        Class<?>[] defaultConfigurations = (Class<?>[]) annotationAttributes.get("defaultConfigurations");
        if (defaultConfigurations != null && defaultConfigurations.length != 0) {
            String name;
            // 拼接名称要用顶部类
            if (metadata.hasEnclosingClass()) {
                name = "default." + metadata.getEnclosingClassName();
            } else {
                name = "default." + metadata.getClassName();
            }
            registerConfiguration(registry, name, defaultConfigurations);
        }
    }

    /**
     * 注册beanDefinition
     *
     * @param name          beanDefinition名称
     * @param configuration 配置类数组
     * @param registry      注册器，默认为DefaultListableBeanFactory
     */
    private void registerConfiguration(BeanDefinitionRegistry registry, String name, Object configuration) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
                .genericBeanDefinition(WbClientSpecification.class);
        // 添加构造参数
        builder.addConstructorArgValue(name)
                .addConstructorArgValue(configuration);
        registry.registerBeanDefinition(name + WbClientSpecification.class.getName(),
                builder.getBeanDefinition());
    }

    public void registerWbClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
        Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableWbClients.class.getName());
        Class<?>[] clients = attrs == null ? null : (Class[]) ((Class[]) attrs.get("clients"));
        if (clients != null && clients.length != 0) {
            Class[] var12 = clients;
            int var14 = clients.length;

            for (int var16 = 0; var16 < var14; ++var16) {
                Class<?> clazz = var12[var16];
                candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
            }
        } else {
            ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
            scanner.setResourceLoader(this.resourceLoader);
            scanner.addIncludeFilter(new AnnotationTypeFilter(WbClient.class));
            Set<String> basePackages = this.getBasePackages(metadata);
            Iterator var8 = basePackages.iterator();

            while (var8.hasNext()) {
                String basePackage = (String) var8.next();
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        }
        //
        Iterator var13 = candidateComponents.iterator();

        while (var13.hasNext()) {
            BeanDefinition candidateComponent = (BeanDefinition) var13.next();
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(), "@WbClient can only be specified on an interface");
                Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(WbClient.class.getCanonicalName());

//                this.registerWbClient(candidateComponent, registry, annotationMetadata, attributes);
//                this.registerWbClientJdk(registry, annotationMetadata, attributes);
//                this.registerWbClientCglib(registry, annotationMetadata, attributes);
                this.registerWbClientByteBuddy(candidateComponent, registry, annotationMetadata, attributes);
            }
        }
    }


    private void registerWbClient(BeanDefinition def, BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        try {

            String className = def.getBeanClassName();
            Assert.notNull(className, "实例类不能为空");
            Class<?> clazz = forName(def.getBeanClassName());

            // 生成代理类
            Class<?> proxyClass = WbClientProxy.buildProxy(clazz);
            // 代理类注册为RootBeanDefinition后放入Spring中
            RootBeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClass(proxyClass);

            ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
                    ? (ConfigurableBeanFactory) registry : null;
            String contextId = getContextId(beanFactory, attributes);

            String[] qualifiers = getQualifiers(attributes);
            if (ObjectUtils.isEmpty(qualifiers)) {
                qualifiers = new String[]{contextId + "WbClient"};
            }

            BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
            BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void registerWbClientJdk(BeanDefinitionRegistry registry, AnnotationMetadata metadata, Map<String, Object> attributes) {

        try {
            String className = metadata.getClassName();
            Class clazz = ClassUtils.resolveClassName(className, null);
            ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
                    ? (ConfigurableBeanFactory) registry : null;
            // 代理feign的工厂类，实现初始化的逻辑
            WbClientJdkFactoryBean factoryBean = new WbClientJdkFactoryBean();

            String contextId = getContextId(beanFactory, attributes);
            String name = getName(attributes);
            factoryBean.setBeanFactory(beanFactory);
            factoryBean.setName(name);
            factoryBean.setType(clazz);
            factoryBean.setPath((String) attributes.get("path"));
            BeanDefinitionBuilder definition = BeanDefinitionBuilder
                    .genericBeanDefinition(clazz, () -> {
                        factoryBean.setUrl(getUrl(beanFactory, attributes));
                        factoryBean.setPath(getPath(beanFactory, attributes));
                        factoryBean.setAsync(getAsync(beanFactory, attributes));
                        factoryBean.setMaxAttempts(getMaxAttempts(beanFactory, attributes));
                        factoryBean.setDecode404(Boolean
                                .parseBoolean(String.valueOf(attributes.get("decode404"))));
                        Object fallback = attributes.get("fallback");
                        if (fallback != null) {
                            factoryBean.setFallback(fallback instanceof Class
                                    ? (Class<?>) fallback
                                    : ClassUtils.resolveClassName(fallback.toString(), null));
                        }
                        Object fallbackFactory = attributes.get("fallbackFactory");
                        if (fallbackFactory != null) {
                            factoryBean.setFallbackFactory(fallbackFactory instanceof Class
                                    ? (Class<?>) fallbackFactory
                                    : ClassUtils.resolveClassName(fallbackFactory.toString(),
                                    null));
                        }
                        return factoryBean.getObject();
                    });
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            definition.setLazyInit(true);

            AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
            beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
            beanDefinition.setAttribute("webClientsRegistrarFactoryBean", factoryBean);

            // has a default, won't be null
            boolean primary = (Boolean) attributes.get("primary");

            beanDefinition.setPrimary(primary);

            String[] qualifiers = getQualifiers(attributes);
            if (ObjectUtils.isEmpty(qualifiers)) {
                qualifiers = new String[] { contextId + "WbClient" };
            }

            BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
            BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private void registerWbClientCglib(BeanDefinitionRegistry registry, AnnotationMetadata metadata, Map<String, Object> attributes) {

        try {
            String className = metadata.getClassName();
            Class clazz = ClassUtils.resolveClassName(className, null);
            ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
                    ? (ConfigurableBeanFactory) registry : null;
            // 代理feign的工厂类，实现初始化的逻辑
            WbClientCglibFactoryBean factoryBean = new WbClientCglibFactoryBean();

            String contextId = getContextId(beanFactory, attributes);
            String name = getName(attributes);
            factoryBean.setBeanFactory(beanFactory);
            factoryBean.setName(name);
            factoryBean.setType(clazz);
            factoryBean.setPath((String) attributes.get("path"));
            BeanDefinitionBuilder definition = BeanDefinitionBuilder
                    .genericBeanDefinition(clazz, () -> {
                        factoryBean.setUrl(getUrl(beanFactory, attributes));
                        factoryBean.setPath(getPath(beanFactory, attributes));
                        factoryBean.setAsync(getAsync(beanFactory, attributes));
                        factoryBean.setMaxAttempts(getMaxAttempts(beanFactory, attributes));
                        factoryBean.setDecode404(Boolean
                                .parseBoolean(String.valueOf(attributes.get("decode404"))));
                        Object fallback = attributes.get("fallback");
                        if (fallback != null) {
                            factoryBean.setFallback(fallback instanceof Class
                                    ? (Class<?>) fallback
                                    : ClassUtils.resolveClassName(fallback.toString(), null));
                        }
                        Object fallbackFactory = attributes.get("fallbackFactory");
                        if (fallbackFactory != null) {
                            factoryBean.setFallbackFactory(fallbackFactory instanceof Class
                                    ? (Class<?>) fallbackFactory
                                    : ClassUtils.resolveClassName(fallbackFactory.toString(),
                                    null));
                        }
                        return factoryBean.getObject();
                    });
            definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
            definition.setLazyInit(true);

            AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
            beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
            beanDefinition.setAttribute("webClientsRegistrarFactoryBean", factoryBean);

            // has a default, won't be null
            boolean primary = (Boolean) attributes.get("primary");

            beanDefinition.setPrimary(primary);

            String[] qualifiers = getQualifiers(attributes);
            if (ObjectUtils.isEmpty(qualifiers)) {
                qualifiers = new String[] { contextId + "WbClient" };
            }

            BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
            BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void registerWbClientByteBuddy(BeanDefinition def, BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        try {

            String className = def.getBeanClassName();
            Assert.notNull(className, "实例类不能为空");
            Class<?> clazz = forName(def.getBeanClassName());

            ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
                    ? (ConfigurableBeanFactory) registry : null;

            // 生成代理类
            Class<?> proxyClass = ByteBuddyProxyFactory.buildProxy(clazz, beanFactory);
            // 代理类注册为RootBeanDefinition后放入Spring中
            RootBeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClass(proxyClass);


            String contextId = getContextId(beanFactory, attributes);

            String[] qualifiers = getQualifiers(attributes);
            if (ObjectUtils.isEmpty(qualifiers)) {
                qualifiers = new String[]{contextId + "WbClient"};
            }

            BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
            BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void registerWbClient3(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {

        String className = annotationMetadata.getClassName();
        Class clazz = ClassUtils.resolveClassName(className, null);
        ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
                ? (ConfigurableBeanFactory) registry : null;
        String contextId = getContextId(beanFactory, attributes);
        String name = getName(attributes);
        WbClientFactoryBean factoryBean = new WbClientFactoryBean();
        factoryBean.setBeanFactory(beanFactory);
        factoryBean.setName(name);
        factoryBean.setContextId(contextId);
        factoryBean.setType(clazz);
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(clazz, () -> {
                    factoryBean.setUrl(getUrl(beanFactory, attributes));
                    factoryBean.setPath(getPath(beanFactory, attributes));
                    factoryBean.setDecode404(Boolean
                            .parseBoolean(String.valueOf(attributes.get("decode404"))));
                    Object fallback = attributes.get("fallback");
                    if (fallback != null) {
                        factoryBean.setFallback(fallback instanceof Class
                                ? (Class<?>) fallback
                                : ClassUtils.resolveClassName(fallback.toString(), null));
                    }
                    Object fallbackFactory = attributes.get("fallbackFactory");
                    if (fallbackFactory != null) {
                        factoryBean.setFallbackFactory(fallbackFactory instanceof Class
                                ? (Class<?>) fallbackFactory
                                : ClassUtils.resolveClassName(fallbackFactory.toString(),
                                null));
                    }
                    return factoryBean.getObject();
                });
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        definition.setLazyInit(true);
        validate(attributes);

        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
        beanDefinition.setAttribute("wbClientsRegistrarFactoryBean", factoryBean);

        // has a default, won't be null
        boolean primary = (Boolean) attributes.get("primary");

        beanDefinition.setPrimary(primary);

        String[] qualifiers = getQualifiers(attributes);
        if (ObjectUtils.isEmpty(qualifiers)) {
            qualifiers = new String[]{contextId + "WbClient"};
        }

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

    String getName(Map<String, Object> attributes) {
        return getName(null, attributes);
    }

    String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
        String name = (String) attributes.get("serviceId");
        if (!StringUtils.hasText(name)) {
            name = (String) attributes.get("name");
        }
        if (!StringUtils.hasText(name)) {
            name = (String) attributes.get("value");
        }
        name = resolve(beanFactory, name);
        return getName(name);
    }


    private String getContextId(ConfigurableBeanFactory beanFactory,
                                Map<String, Object> attributes) {
        String contextId = (String) attributes.get("contextId");
        if (!StringUtils.hasText(contextId)) {
            return getName(attributes);
        }

        contextId = resolve(beanFactory, contextId);
        return getName(contextId);
    }

    private String resolve(ConfigurableBeanFactory beanFactory, String value) {
        if (StringUtils.hasText(value)) {
            if (beanFactory == null) {
                return this.environment.resolvePlaceholders(value);
            }
            BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver();
            String resolved = beanFactory.resolveEmbeddedValue(value);
            if (resolver == null) {
                return resolved;
            }
            return String.valueOf(resolver.evaluate(resolved,
                    new BeanExpressionContext(beanFactory, null)));
        }
        return value;
    }

    private String getUrl(ConfigurableBeanFactory beanFactory,
                          Map<String, Object> attributes) {
        String url = resolve(beanFactory, (String) attributes.get("url"));
        return getUrl(url);
    }

    private String getPath(ConfigurableBeanFactory beanFactory,
                           Map<String, Object> attributes) {
        String path = resolve(beanFactory, (String) attributes.get("path"));
        return getPath(path);
    }

    private Boolean getAsync(ConfigurableBeanFactory beanFactory,
                           Map<String, Object> attributes) {
        return (Boolean) attributes.get("async");
    }

    private Integer getMaxAttempts(ConfigurableBeanFactory beanFactory,
                             Map<String, Object> attributes) {
        return (Integer) attributes.get("maxAttempts");
    }


    protected ClassPathScanningCandidateComponentProvider getScanner() {
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                boolean isCandidate = false;
                if (beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation()) {
                    isCandidate = true;
                }

                return isCandidate;
            }
        };
    }

    protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
        Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableWbClients.class.getCanonicalName());
        Set<String> basePackages = new HashSet();
        String[] var4 = (String[]) ((String[]) attributes.get("value"));
        int var5 = var4.length;

        int var6;
        String pkg;
        for (var6 = 0; var6 < var5; ++var6) {
            pkg = var4[var6];
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }

        var4 = (String[]) ((String[]) attributes.get("basePackages"));
        var5 = var4.length;

        for (var6 = 0; var6 < var5; ++var6) {
            pkg = var4[var6];
            if (StringUtils.hasText(pkg)) {
                basePackages.add(pkg);
            }
        }

        Class[] var8 = (Class[]) ((Class[]) attributes.get("basePackageClasses"));
        var5 = var8.length;

        for (var6 = 0; var6 < var5; ++var6) {
            Class<?> clazz = var8[var6];
            basePackages.add(ClassUtils.getPackageName(clazz));
        }

        if (basePackages.isEmpty()) {
            basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
        }

        return basePackages;
    }


    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;

    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }


    private String getQualifier(Map<String, Object> client) {
        if (client == null) {
            return null;
        }
        String qualifier = (String) client.get("qualifier");
        if (StringUtils.hasText(qualifier)) {
            return qualifier;
        }
        return null;
    }

    private String[] getQualifiers(Map<String, Object> client) {
        if (client == null) {
            return null;
        }
        List<String> qualifierList = new ArrayList<>(
                Arrays.asList((String[]) client.get("qualifiers")));
        qualifierList.removeIf(qualifier -> !StringUtils.hasText(qualifier));
        if (qualifierList.isEmpty() && getQualifier(client) != null) {
            qualifierList = Collections.singletonList(getQualifier(client));
        }
        return !qualifierList.isEmpty() ? qualifierList.toArray(new String[0]) : null;
    }

    private void validate(Map<String, Object> attributes) {
        AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
        // This blows up if an aliased property is overspecified
        // FIXME annotation.getAliasedString("name", WbClient.class, null);
        validateFallback(annotation.getClass("fallback"));
        validateFallbackFactory(annotation.getClass("fallbackFactory"));
    }

    private Map<String, Object> getAnnotationAttributes(AnnotationMetadata metadata, Class<?> clazz) {
        Map<String, Object> annotationAttributes = metadata.
                getAnnotationAttributes(clazz.getName());

        return annotationAttributes;
    }

    private String getFeignName(Map<String, Object> feignAnnotatedMetadata) {
        String name;
        name = (String) feignAnnotatedMetadata.get("value");
        if (org.apache.commons.lang3.StringUtils.isBlank(name)) {
            name = (String) feignAnnotatedMetadata.get("name");
        }
        return name;
    }

}
